package interp;

import java.util.HashMap;

import ic.ast.Visitor;
import ic.ast.decl.ClassType;
import ic.ast.decl.DeclClass;
import ic.ast.decl.DeclField;
import ic.ast.decl.DeclLibraryMethod;
import ic.ast.decl.DeclMethod;
import ic.ast.decl.DeclStaticMethod;
import ic.ast.decl.DeclVirtualMethod;
import ic.ast.decl.Parameter;
import ic.ast.decl.PrimitiveType;
import ic.ast.decl.Program;
import ic.ast.decl.Type;
import ic.ast.expr.BinaryOp;
import ic.ast.expr.BinaryOp.BinaryOps;
import ic.ast.expr.Expression;
import ic.ast.expr.ExpressionBlock;
import ic.ast.expr.Length;
import ic.ast.expr.Literal;
import ic.ast.expr.NewArray;
import ic.ast.expr.NewInstance;
import ic.ast.expr.Ref;
import ic.ast.expr.RefArrayElement;
import ic.ast.expr.RefField;
import ic.ast.expr.RefVariable;
import ic.ast.expr.StaticCall;
import ic.ast.expr.This;
import ic.ast.expr.UnaryOp;
import ic.ast.expr.VirtualCall;
import ic.ast.stmt.LocalVariable;
import ic.ast.stmt.Statement;
import ic.ast.stmt.StmtAssignment;
import ic.ast.stmt.StmtBlock;
import ic.ast.stmt.StmtBreak;
import ic.ast.stmt.StmtCall;
import ic.ast.stmt.StmtContinue;
import ic.ast.stmt.StmtIf;
import ic.ast.stmt.StmtReturn;
import ic.ast.stmt.StmtWhile;

/* Important Remark : we first go through semantic checker, so not all errors are checked here */

public class InterpertVisitor implements Visitor {
	
	
	HashMap<String,Object> vars;
	
	public InterpertVisitor(DeclMethod method,HashMap<String,Object> vars){
		this.vars=vars;
		method.accept(this);
	}
	
	
	
	public Object visit(DeclMethod declMethod){
		
		for(Statement statement:declMethod.getStatements()){
			statement.accept(this);
		}
		
		return null;
	}
	
	public Object visit(Program program) { 
		throw new RuntimeError("Runtime Error");
	}

	public Object visit(DeclClass icClass){ 
		throw new RuntimeError("Runtime Error");
	}

	public Object visit(DeclField field){
		throw new RuntimeError("Runtime Error");
	}

	public Object visit(DeclVirtualMethod method){ 
		for(Statement statement:method.getStatements()){
			statement.accept(this);
		}
		return null;
	}

	public Object visit(DeclStaticMethod method){ 
		for(Statement statement:method.getStatements()){
			statement.accept(this);
		}
		return null;
	}

	public Object visit(DeclLibraryMethod method){ 
		throw new RuntimeError("Runtime Error");
	}

	public Object visit(Parameter formal){ 
		throw new RuntimeError("Runtime Error");
	}
	public Object visit(PrimitiveType type){ return null; }

	public Object visit(ClassType type){ return null; }

	public Object visit(StmtAssignment assignment){
		Ref reference=assignment.getVariable();
		
		if (reference instanceof RefVariable){
			if(vars.get(((RefVariable) reference).getName()) instanceof ArrayWrapper){ //if this variable refers to an array, we are trying to initalize it
				ArrayWrapper arr=(ArrayWrapper)(vars.get(((RefVariable) reference).getName()));
				NewArray newarr=(NewArray)(assignment.getAssignment());
				Integer arrSize=(Integer)(newarr.getSize().accept(this));
				arr.initalizeSize(arrSize);
				return null;
			} else { // just a regular variable 
				vars.put(((RefVariable)reference).getName(),assignment.getAssignment().accept(this));
				return null;
			}
		}
		else if (reference instanceof RefArrayElement){
			Integer index=(Integer)(((RefArrayElement)reference).getIndex().accept(this));
			RefVariable array_ref=(RefVariable)(((RefArrayElement)reference).getArray());
			String name=array_ref.getName();
			ArrayWrapper arrWrap=(ArrayWrapper)(vars.get(name));
			if(arrWrap.getSize()<=index){
				throw new RuntimeError("Array index out of bounds");
			}
			arrWrap.setValue(assignment.getAssignment().accept(this), index);
		}
		else{
			throw new RuntimeError("Reference to illegal variable");
		}
		return null;
	}

	public Object visit(StmtCall callStatement){ 
		throw new RuntimeError("Not allowed to call functions");
	}

	public Object visit(StmtReturn returnStatement){
		Expression value=returnStatement.getValue();
		System.out.println(value.accept(this));
		return null;
	}

	public Object visit(StmtIf ifStatement){
		if( ((Boolean)(ifStatement.getCondition().accept(this))).booleanValue()){
			return ifStatement.getOperation().accept(this);
		}
		else if (ifStatement.getElseOperation()!=null){
			return ifStatement.getElseOperation().accept(this);
		}
		return null;
	}

	public Object visit(StmtWhile whileStatement){ 
	
		while(    ((Boolean)(whileStatement.getCondition().accept(this))).booleanValue() == true ){
			Object obj=whileStatement.getOperation().accept(this);
			if(obj instanceof StmtBreak){
				break;
			} else if (obj instanceof StmtContinue){
				continue;
			}
		}
		
		return null;
	}

	public Object visit(StmtBreak breakStatement){ 
		
		return breakStatement;
	}

	public Object visit(StmtContinue continueStatement){ 

		return continueStatement;
	}

	public Object visit(StmtBlock statementsBlock){ 
		for(Statement stmt:statementsBlock.getStatements()){
			Object obj=stmt.accept(this);
			if(obj instanceof StmtBreak || obj instanceof StmtContinue){
				return obj;
			}
		}
		return null;
	}

	public Object visit(LocalVariable localVariable){
		Type varType=localVariable.getType();
		
		if(varType.getArrayDimension()==1){ 
			if(localVariable.isInitialized()){
				try {
					NewArray arrInit=(NewArray)(localVariable.getInitialValue());
					Integer arrSize=(Integer)(arrInit.getSize().accept(this));
					vars.put(localVariable.getName(),new ArrayWrapper(((PrimitiveType)varType).getDisplayName(),arrSize));
					return null;
				}
				catch (ClassCastException cce){
					throw new RuntimeError("Runtime Error");
				}
			}
			else{
				vars.put(localVariable.getName(),new ArrayWrapper(((PrimitiveType)varType).getDisplayName()));
				return null;
			}
			
		}
		else if (varType.getArrayDimension()==0)
		{
			if (localVariable.isInitialized()){
				vars.put(localVariable.getName(), localVariable.getInitialValue().accept(this));
			}
			else{
				
				String type_str=((PrimitiveType)varType).getDisplayName();
				switch(type_str){
				case ("int"):
					vars.put(localVariable.getName(),new Integer(0));
					return null;
				case ("boolean"):
					vars.put(localVariable.getName(),new Boolean(false));
					return null;
				case ("string"):
					vars.put(localVariable.getName(),new DummyValue());
				}
			
			}
		}
		else{
			throw new RuntimeError("Only 1 - dimension arrays are allowed");
		}
		
		return null;
	}

	public Object visit(RefVariable location){
		return vars.get(location.getName());
	}

	public Object visit(RefField location){ 
		return null; 
	} 

	public Object visit(RefArrayElement location){ 
		Integer index=(Integer)(location.getIndex().accept(this));
		String array_name=((RefVariable)(location.getArray())).getName();
		ArrayWrapper arr=(ArrayWrapper)(vars.get(array_name));
		if(arr.getSize()<=index){
			throw new RuntimeError("Array index out of bounds");
		}
		return arr.getValue(index);
		
	}

	public Object visit(StaticCall call){ 
		throw new RuntimeError("Not allowed to call functions");
	}

	public Object visit(VirtualCall call){ 
		throw new RuntimeError("Not allowed to call functions");
	}

	public Object visit(This thisExpression){ 
		throw new RuntimeError("Invalid 'this' while interpreting");
	}

	public Object visit(NewInstance newClass){ 
		throw new RuntimeError("Not allowed to use new Classes while interpreting");
	}

	public Object visit(NewArray newArray){ 
		return null; 
	}

	public Object visit(Length length){ 
		throw new RuntimeError("Not allowed to use length while interpreting");
	}

	public Object visit(Literal literal){
		switch(literal.getType().toString()){
		case ("int"):
			return Integer.parseInt((String)(literal.getValue()));
		case ("boolean"):
			if(((String)literal.getValue()).equals("true")){
				return new Boolean(true);
			}
			else{
				return new Boolean(false);
			}
		}
		return (String)(literal.getValue());
	}

	public Object visit(UnaryOp unaryOp){ 
		if(unaryOp.getOperator().toString().equals("!")){
			if( ((Boolean)(unaryOp.getOperand().accept(this))).booleanValue() == true ){
				return new Boolean(false);
			}
			else{
				return new Boolean(true);
			}
		}
		else{
			return new Integer((-1)*((Integer)unaryOp.getOperand().accept(this)));
		}
	}
	
	public Object visit(BinaryOp binaryOp){ 
		Object first_operand=binaryOp.getFirstOperand().accept(this);
		Object second_operand=binaryOp.getSecondOperand().accept(this);
		String operatorString=binaryOp.getOperator().toString();
		
		if(operatorString.equals("+")){
			if(first_operand instanceof Integer){
				return new Integer( ((Integer)first_operand) + ((Integer)second_operand) );
			}
			else {
				return new String( ((String)first_operand) +((String)second_operand)  );
			}
		} else if (operatorString.equals("-")){
			return new Integer ( ((Integer)first_operand) - ((Integer)second_operand)  );
		} else if (operatorString.equals("*")){
			return new Integer ( ((Integer)first_operand) * ((Integer)second_operand) );
		} else if (operatorString.equals("/")){
			return new Integer ( ((Integer)first_operand) / ((Integer)second_operand) );
		} else if (operatorString.equals("%")){
			return new Integer ( ((Integer)first_operand) % ((Integer)second_operand) );
		} else if (operatorString.equals("&&")) {
			return new Boolean( ((Boolean)first_operand).booleanValue() && ((Boolean)second_operand).booleanValue() );
		} else if (operatorString.equals("||")) {
			return new Boolean( ((Boolean)first_operand).booleanValue() || ((Boolean)second_operand) );
		} else if (operatorString.equals("<")) {
			return new Boolean( ((Integer)first_operand).intValue() < ((Integer)second_operand).intValue() );
		} else if (operatorString.equals("<=")) {
			return new Boolean( ((Integer)first_operand).intValue() <= ((Integer)second_operand).intValue() );
		} else if (operatorString.equals(">")) {
			return new Boolean( ((Integer)first_operand).intValue() > ((Integer)second_operand).intValue() );
		} else if (operatorString.equals(">=")) {
			return new Boolean( ((Integer)first_operand).intValue() >= ((Integer)second_operand).intValue() );
		} else if (operatorString.equals("==")) {
			return new Boolean( ((Integer)first_operand).intValue() == ((Integer)second_operand).intValue() );
		} else if (operatorString.equals("!=")) {
			return new Boolean( ((Integer)first_operand).intValue() != ((Integer)second_operand).intValue() );
		}
			
		return null;
	}

	public Object visit(ExpressionBlock expressionBlock){ 
		return expressionBlock.getExpression().accept(this);
	}
	
	
}
